package de.dnb.gnd.parser;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.naming.OperationNotSupportedException;

import de.dnb.basics.Constants;
import de.dnb.basics.applicationComponents.NullIterator;
import de.dnb.basics.applicationComponents.RangeCheckUtils;
import de.dnb.gnd.exceptions.IllFormattedLineException;
import de.dnb.gnd.parser.line.Line;
import de.dnb.gnd.parser.line.LineParser;
import de.dnb.gnd.parser.tag.GNDTagDB;
import de.dnb.gnd.parser.tag.Tag;
import de.dnb.gnd.parser.tag.TagDB;
import de.dnb.gnd.utils.RecordUtils;

/**
 * Kann verändert werden, daher clone().
 * 
 * @author Christian
 *
 */
public final class Record implements Iterable<Line>, Cloneable {

	/*private*/Map<Tag, Field> fieldMap = new TreeMap<Tag, Field>();

	String rawData = "";

	private final String id;

	public final TagDB tagDB;

	public Record(final String id, TagDB tagDB) {
		super();
		this.id = id;
		this.tagDB = tagDB;
	}

	@Override
	public Record clone() {
		Record cloned;
		try {
			cloned = (Record) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new InternalError(e.getMessage());
		}
		Map<Tag, Field> newFieldMap = new TreeMap<Tag, Field>();
		cloned.fieldMap = newFieldMap;
		Set<Map.Entry<Tag, Field>> entryset = fieldMap.entrySet();
		for (Entry<Tag, Field> entry : entryset) {
			Tag tag = entry.getKey();
			Field field = entry.getValue().clone();
			newFieldMap.put(tag, field);
		}

		return cloned;
	}

	/**
	 * Gibt eine neue Liste aller Felder, die  unbedenklich
	 * verändert werden kann.
	 * 
	 * @return	Ein Clone der Felder.
	 */
	public Collection<Field> getFields() {
		Collection<Field> fields = fieldMap.values();
		List<Field> newFields = new LinkedList<Field>();
		for (Field field : fields) {
			newFields.add(field.clone());
		}
		return newFields;
	}

	/**
	 * Gibt ein Clone eines Feldes oder null. Der ursprüngliche
	 * Datensatz kann unbedenklich verändert werden.
	 *  
	 * @param tag	nicht null
	 * @return		Feld oder null.
	 */
	public Field getField(final Tag tag) {
		RangeCheckUtils.assertReferenceParamNotNull("tag", tag);
		if (tag == null)
			throw new IllegalArgumentException("Tag ist null");
		Field field = fieldMap.get(tag);
		if (field == null)
			return null;
		else
			return field.clone();
	}

	/**
	 * Alias für set().
	 * 
	 * @param field	nicht null.
	 */
	public void add(final Field field) {
		RangeCheckUtils.assertReferenceParamNotNull("field", field);
		setField(field);
	}

	/**
	 * Fügt eine Zeile zum Datensatz hinzu.
	 * 
	 * @param line  nicht null
	 * @throws OperationNotSupportedException   
	 *          Wenn weder die Zeile noch ihr Inhalt zum Datensatz hinzugefügt 
	 *          werden darf.
	 */
	public void add(final Line line) throws OperationNotSupportedException {
		RangeCheckUtils.assertReferenceParamNotNull("line", line);
		Tag tag = line.getTag();
		Field field = fieldMap.get(tag);
		if (field == null) {
			if (!tagDB.contains(tag))
				throw new IllegalArgumentException("tag der zeile " + line
					+ " ist nicht in der Datenbank von Datensatz "
					+ this.id + " vorhanden");
			field = new Field(line);
			fieldMap.put(tag, field);
		} else
			try {
				field.add(line);
			} catch (OperationNotSupportedException e) {
				throw new OperationNotSupportedException(e.getMessage()
					+ " ; idn: " + id + " Zeile: " + line);
			}
	}

	/**
	 * Entfernt das zu tag gehörige Feld, sofern vorhanden.
	 * 
	 * @param tag	beliebig.
	 */
	public void removeField(final Tag tag) {
		if (tag != null)
			fieldMap.remove(tag);
	}

	/**
	 * (Er)setzt das zum tag von field gehörige Feld.
	 * 
	 * @param field nicht null.
	 */
	public void setField(final Field field) {
		RangeCheckUtils.assertReferenceParamNotNull("field", field);
		fieldMap.put(field.tag, field);
	}

	/**
	 * Entfernt eine Zeile vom Datensatz. Sollte das zugehörige Feld auch leer
	 * sein, wird auch dieses entfernt.
	 * 
	 * @param line	nicht null.
	 * @return	ob die Zeile entfernt werden konnte.
	 */
	public boolean remove(final Line line) {
		Tag tag = line.getTag();
		Field field = fieldMap.get(tag);
		boolean removed = field.remove(line);
		// leere Felder ganz entfernen:
		if (field.getLines().isEmpty())
			fieldMap.remove(tag);
		return removed;
	}

	public String getRawData() {
		return rawData;
	}

	/**
	 * Gibt den Inhalt des Feldes 003@. Wenn nicht vorhanden, die
	 * anderweitig geparste ID.
	 * 
	 * @return	ID oder null, wenn nicht ermittelbar.
	 */
	public String getId() {
		String id = RecordUtils.getContentOfSubfield(this, "003@", '0');
		if (id != null)
			return id;
		else
			return this.id;
	}

	/**
	 * Gibt die vergebenen Tags.
	 * 
	 * @return	neue Menge, die bedenkenlos geändert werden kann.
	 */
	public LinkedHashSet<Tag> getTags() {
		return new LinkedHashSet<Tag>(fieldMap.keySet());
	}

	final class MyIterator implements Iterator<Line> {

		private MyIterator(final Format format) {
			if (format == Format.PICA_PLUS) {
				Collection<Field> theFields = getFields();
				TreeSet<Field> set =
					new TreeSet<Field>(new Field.PicaPlusComparator());
				set.addAll(theFields);
				Iterator<Field> iteratorF = set.iterator();
				fieldsIterator = iteratorF;
			} else
				fieldsIterator = getFields().iterator();
			lineIterator = new NullIterator<Line>();
		}

		private Iterator<Line> lineIterator;

		private Iterator<Field> fieldsIterator;

		@Override
		public boolean hasNext() {
			/*
			 * Felder sind per definitionem nicht leer (enthalten mindestens
			 * eine Zeile)
			 */
			return fieldsIterator.hasNext() || lineIterator.hasNext();
		}

		@Override
		public Line next() {
			if (!lineIterator.hasNext()) {
				/*
				 * Ist möglich, da fieldsIterator.hasNext() garantiert, dass
				 * da mindestens noch ein Feld in fieldMap vorhanden ist. 
				 * Da aber Felder per definitionem nicht leer sind, enthält das 
				 * nächste Feld immer noch eine Zeile:
				 */
				lineIterator = fieldsIterator.next().iterator();
			}
			return lineIterator.next();
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}

	}

	@Override
	public Iterator<Line> iterator() {
		return new MyIterator(Format.PICA3);
	}

	public Iterator<Line> picaPlusIterator() {
		return new MyIterator(Format.PICA_PLUS);
	}

	@Override
	public String toString() {
		String s = "IDN: " + id;
		for (Line line : this) {
			s += Constants.LINE_SEPARATOR + line;
		}
		return s;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		// fieldMap dürfen nicht einbezogen werden, da veränderlich!
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		return result;
	}

	@Override
	public boolean equals(final Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof Record)) {
			return false;
		}
		Record other = (Record) obj;
		if (fieldMap == null) {
			if (other.fieldMap != null) {
				return false;
			}
		} else if (!fieldMap.equals(other.fieldMap)) {
			return false;
		}
		if (id == null) {
			if (other.id != null) {
				return false;
			}
		} else if (!id.equals(other.id)) {
			return false;
		}
		return true;
	}

	/**
	 * @param args
	 * @throws IllFormattedLineException 
	 * @throws OperationNotSupportedException 
	 */
	public static void main(String[] args)
		throws IllFormattedLineException,
		OperationNotSupportedException {
		Record record = new Record("12345678", GNDTagDB.getDB());
		Tag tag = GNDTagDB.getDB().findTag("008");
		Line line = LineParser.parse(tag, Format.PICA3, "wim", false);
		record.add(line);
		System.out.println(record);
		System.out.println(RecordUtils.toPica(record, Format.PICA_PLUS,
			true, Constants.LINE_SEPARATOR, '$'));
		System.out.println(Constants.LINE_SEPARATOR.equals("\r\n"));
	}

}
